#!/usr/bin/env python
# -*- coding: utf-8 -*-

'''
    算头不算尾，计算利息时，存款天数一律算头不算尾，即从存入日起算至取款前一天止(活期计息日除外)；
    对年、对月、对日计算，各种定期存款的到期日均以对年、对月、对日为准。即自存入日至次年同月同日为一对年,
        存入日至下月同一日为对月；若超过了当月最大天数，以当月月末为到期日
    定期储蓄到期日，比如遇例假不办公，可以提前一日支取，视同到期计算利息，手续同提前支取办理       

    2005/9/21对活期结息方式进行了调整: 按年结息改为按季结息且存期按实际天数计算(参见银发[2005]129号)
    活期目前是每季度末月的20日结息一次(各行的计息日可能不同),结转的利息在下个季度就会产生复利
    人民币业务的利率换算公式为：日利率(‰)=年利率（%）÷360
                                月利率(‰)=年利率（%）÷12
        活期的日利率也是除以360,虽然调整后存期是按实际天数计算
    定期期满时结息.如果有自动转存,本息合并转为下一笔新定期的本金:没有自动转存,本息合并转为活期的本金    
    储蓄存款利息计算时，本金以“元”为起息点，元以下的角、分不计息;利息的金额算至分位，分位以下四舍五入;
        分段计息算至厘位，合计利息后分以下四舍五入    
    '''    
import pdb
from datetime import date,timedelta
import calendar as Cdate
import re , os ,csv
import lsupdate_mul as lsupdate
from operator import attrgetter  # 用于费率列表排序(按日期)

def _get_path(fn):
    dir = os.path.dirname(__file__)
    return os.path.join(dir,fn)
    
_RATES_FILE = _get_path('ls.txt')       # 默认费率文件
_SAVE_FILE  = _get_path('orecord.txt')  # 默认的清单文件
    
class Rates:                 # 特定日期费率
    __slots__ = ('__day','__rates')
    def __init__(self,day,rates):
        self.__day = day          # 调整费率的日期
        self.__rates = rates      # 新费率 一个float数(税率)或一个tuple(利率)
    @property
    def rates(self):
        return self.__rates
    @property
    def day(self):
        return self.__day
    def __eq__(self,other):       # 去重复
        return self.__day == other.__day
    def __str__(self):
        if isinstance(self.__rates,tuple) :         # 利率
            s = '{} {}'.format(self.__day,' '.join('{0:6.2f}'.format(i) for i in self.__rates))            
        else :
            s = '{} {:6.2f}'.format(self.__day,self.__rates)# 税率
        return s
    def __repr__(self):  # for csv
        if isinstance(self.__rates,tuple) :         # 利率
            s = '{},{}\n'.format(self.__day,','.join('{0:6.2f}'.format(i) for i in self.__rates))            
        else :
            s = '{},{:6.2f}\n'.format(self.__day,self.__rates)# 税率
        return s    
    def __hash__(self):        
        return hash(self.__day)
    
class Irecord:      # 存单记录
    __slots__ = ('__iday','__money','__month','__again','__num')
    
    def __init__(self,iday,n=0,*,money,month,again): # 关键字参数
        self.__iday = iday           # 存入时间
        self.__money = money         # 本金
        self.__month = month         # 存储期限
        self.__again = again         # 转存期限
        self.__num   = n
    @property
    def day(self):
        return self.__iday
    @property
    def money(self):
        return self.__money
    @property
    def month(self):
        return self.__month
    @property
    def again(self):
        return self.__again
    @property
    def num(self):
        return self.__num
    @num.setter
    def num(self,n):
        self.__num = n
        
    def __str__(self):
        s = '编号:{} 存入时间: {!s} 本金:{:.2f} 存期: {:2d} 转存: {:2d}'
        return s.format(self.__num,self.__iday,self.__money,self.__month,self.__again)
    
    def checkMonth(self):
        return (self.month in ratesTable.INDEX_L and self.again in ratesTable.INDEX_L)
    
def strToird(irstr):
    pat = re.compile(r'\d+\.?\d*')
    res = pat.findall(irstr)
    if len(res) == 6:
        return Irecord(date(*map(int,res[:3])),money=float(res[3]),month=int(res[4]),again=int(res[5]))
    return None
        
class ratesTable:           # 费率表
    INDEX_L = {0:0,3:1,6:2,12:3,24:4,36:5,60:6}
    
    def __init__(self,lsfile=_RATES_FILE):
        self.tableInit(lsfile)     
        
    # def match_3(self,Bool=0) : # 在升序费率表中匹配<=day的最大值 生成器 二分法
    #     h = None
    #     con = self._icon if Bool else self._tcon
    #     l = len(con) - 1
    #     while 1:
    #         day = yield h      # 返回值小于零说明day最小,此时费率应设为0
    #         low , h = 0 , 1
    #         while low <= h:
    #             mid = low + (h - low )// 2
    #             if day < con[mid].day :
    #                 h = mid - 1
    #             elif day > con[mid].day :
    #                 low = mid + 1
    #             else : 
    #                 h = mid         # 精确匹配
    #                 break
    def match_bb(self,bl=0):# 在升序费率表中匹配<=day的最大值 闭包 二分法
        con = self._icon if bl else self._tcon
        l = len(con) - 1
        def match(day):
            nonlocal con,l
            low , h = 0 , l
            while low <= h:
                mid = low + (h - low )// 2
                if day < con[mid].day :
                    h = mid - 1
                elif day > con[mid].day :
                    low = mid + 1
                else : 
                    h = mid         # 精确匹配
                    break
            return h    # 返回值小于零说明day最小,此时费率应设为0
        def update():   # 联网更新后的状态更新
            nonlocal con,l
            con = self._icon if bl else self._tcon
            l = len(con) - 1
        match.update = update
        return match
        
    def setcor(self):
        if hasattr(self,'icor'):
            self.icor.update()
            self.tcor.update()
        else:
            self.icor = self.match_bb(1)    # 利率查询 闭包
            self.tcor = self.match_bb()    
        
    def empty(self):
        return (not self._icon) or (not self._tcon)
        
    def getIcon(self):
        return self._icon
        
    def getTcon(self):
        return self._tcon
        
    def tableInit(self,fileName):
        try:
            with open(fileName) as f:
                sl,st = set(),set()
                if fileName.endswith('.csv'):
                    fcsv = csv.reader(f)
                    next(fcsv)  # kip header
                    for line in fcsv:
                        s = len(line)
                        if s == 8:
                            d = date(*map(int,re.split(r'\D+',line[0])))
                            irates = Rates(d,tuple(map(float,line[1:])))
                            sl.add(irates)
                        elif s == 2:
                            d = date(*map(int,re.split(r'\D+',line[0])))
                            trate = Rates(d,float(line[1])/100)
                            st.add(trate)
                else:
                    pat = re.compile(r'\d+\.?\d*')
                    pat_head = re.compile(r'\s*[LSls]\s')
                    # r=re.findall(r'[..0-9]+',line)  
                    for line in f:
                        # if line.lstrip().startswith(('L','S','l','s')):
                                # r = line.split()
                        if pat_head.match(line):
                            r = pat.findall(line)
                            s = len(r)
                            if s == 10 :        # 利率
                                d = date(int(r[0]),int(r[1]),int(r[2]))  
                                irates = Rates(d,tuple(map(float,r[3:])))
                                sl.add(irates)
                            elif s == 4 :       # 税率
                                d = date(int(r[0]),int(r[1]),int(r[2]))
                                trate = Rates(d,float(r[3])/100)
                                st.add(trate)
                            else:
                                self.InitFORempty()
                                return
        except Exception:
            self.InitFORempty()
        else :
            self._icon = sorted(sl,key=attrgetter('day')) # 升序 _Rates__day
            self._tcon = sorted(st,key=attrgetter('day'))
            self.setcor()
            
    def strTable(self):
        s = 'L {}\nS {}'.format('\nL '.join(map(str,self._icon)),'\nS '.join(map(str,self._tcon)))
        return s
        
    def _ratesToCsv(self,tocsvFile):
        with open(tocsvFile,'w') as f:
            f.write('修改日,活期,3个月,6个月,12个月,24个月,36个月,60个月\n')
            f.writelines(map(repr,self.getTcon()))
            # f.writelines(map(repr,self.getIcon()))
        
    def getTheIrate(self,i,month):   # 获取与monthon对应的利率
        try:
            return (self._icon[i]).rates[self.__class__.INDEX_L[month]]
        except KeyError:
            return 0.0

    def InitFORempty(self):
        self._icon = []  # 利率表
        self._tcon = []  # 税率表

def toCsvFile(csvfile):        # 生成利税表的CSV文件
    table = ratesTable()       # 前提是有 ls.txt文件
    table._ratesToCsv(csvfile)
    
def dateAddmonth(day,month):   #日期加上月数（存期）
    y = day.year
    m = day.month + month
    d = day.day
    if m > 12 :
        y += (m//12)
        m %= 12                # m 可能是12的整数倍
        if m == 0 :
            m = 12
            y -= 1
    tmp = Cdate.monthrange(y,m)[1]  # 当月最大天数
    if d > tmp :
        d = tmp
    return date(y,m,d)
    
def days360(iday,oday): #  日期间相差天数(一年按360天算)
    m = oday.month - iday.month
    di = iday.day
    do = oday.day
    imax = Cdate.monthrange(iday.year,iday.month)[1]
    if do == 31 :
    # if do == Cdate.monthrange(oday.year,oday.month)[1] :
        if di < imax:
            do = 1
            m += 1
        else:
            do = 30
    if di == imax :
        di = 30
    return ((oday.year - iday.year)*360 + m*30 + (do-di)) # 大陆法

def days36x(iday,oday):
    return oday.toordinal() - iday.toordinal()

def _dayRate(yRate):            # 年利率化为日利率
    return  yRate / (100*360)

def _monthRate(yRate):          # 年利率化为月利率
    return  yRate / (100*12)

def _preint(irate,money, month):# 定期一个存期税前息(区间)    
    return int(money) * _monthRate(irate) * month
    
def _daysFunc(oday,real,lump_1=False):  
        ''' 活期的存期核心算法   oday:取日  
              前提：假设2005/9/21日以前都不是按实际天数算的存期
              lump_1: 为False表示是按活期存入的或定期未满一个存期就提前支取
                      为True表示定期起码存满了一个存期才提前支取
              real：为真时一般按实际天数计算,例外之一是前面“前提”所指,其二是lump_1为True
        '''
        return days360 if not real or oday <= date(2005,9,21) or lump_1 else days36x  

def _Jday(preDay,stday):      # 计息日
    # stday = date(2005,6,30) # 结息方式调整前的最后一个计息日 调整日为2005/9/21
    if preDay <= stday:
        jday = date(preDay.year,6,30)   
        while jday < preDay:
            jday = dateAddmonth(jday,12)# 调整前按年结息
    else: 
        jday = date(2005,12,20) if preDay.year == 2005 else date(preDay.year,3,_JXR_DAY)
        while jday < preDay:
            jday = dateAddmonth(jday,3) # 调整后按季结息
    return jday

class lumpCalc:         # 活期及整存整取算法
    _JXR_DAY = 20       # 计息日为每季末的第20日
    
    def __init__(self,table):
        self.table = table  
            
    def _tax_MUL_days(self,preDay,posDay,daysFunc,jxr):
        # 各区间税率x天数之和 preDay <= postDay
        i = self.table.tcor(preDay)
        tcon = self.table.getTcon()
        iend = len(tcon)-1
        sum_ = 0.0
        tmp = preDay
        oday = (posDay + timedelta(days=1)) if jxr else posDay  # 计息日加1
        while i < iend :
            if oday > tcon[i+1].day : 
                if i >= 0 :                 
                    trate = tcon[i].rates
                    sum_ += daysFunc(tmp,tcon[i+1].day)*(trate)
                i += 1            
                tmp = tcon[i].day
            else :
                break
        if i >= 0:
            trate = tcon[i].rates
            sum_ += daysFunc(tmp,oday)*(trate)
        return sum_
    
    def tax(self,preDay,posDay,money,irate,daysFunc=days360,jxr=False):
        return int(money)*_dayRate(irate)*self._tax_MUL_days(preDay,posDay,daysFunc,jxr)
        # 本金X日利率X(各利率期间需要计税的天数X各相应税率之和)     
    
    def cur_tax_later_sep(self,iday,oday,money,daysFunc,sep,jxr=False):  # 活期税后息
        i = self.table.icor(oday)
        r = 0.0 if i<0 else self.table.getTheIrate(i,0)
        if sep is None:
            i = self._tax_MUL_days(iday,oday,daysFunc,jxr)  # 借用
            return int(money)*_dayRate(r)*(daysFunc(iday,oday)+(1 if jxr else 0) - i)
                                       # +1：计息日算头又算尾
        sqx = int(money)*_dayRate(r) * (daysFunc(iday,oday)+ (1 if jxr else 0))
        s = self.tax(iday,oday,money,r,daysFunc,jxr)
        sep.append([iday,oday,money,r,sqx,s])
        return sqx - s
    
    def curCalc_sep(self,record,oday,sep,real): # 活期本息和(含计息日结息)
        '''以下测试活期计息方式调整前后的对比 (按一年比对如下)：
           a: 2004/7/1--2005/7/1    本金10000 按年结息 本息和为 10057.60  <---days360()
           b: 2005/9/21--2006/9/21  本金10000 按季结息 本息和为 10057.72  <---days360()
           c: 2005/9/21--2006/9/21  本金10000 按季结息 本息和为 10058.52  <---days36x()           
           a为调整之前的测试    b/c为调整之后按不同存期计算方式进行测试
        '''        
        stday = date(2005,6,30) # 结息方式调整前的最后一个计息日 调整日为2005/9/21
        pre = record.day
        jday = _Jday(pre,stday)
        
        sum_ = round(record.money,2)
        sep = [] if sep else None
        while jday <= oday:     # 计息日当日清户是算头又算尾吗?
            sum_ += round(self.cur_tax_later_sep(pre,jday,sum_,_daysFunc(jday,real),sep,True),3)           
            pre = jday + timedelta(days=1)      # 上一步算头又算尾,此处前进一天,避免重复
            if jday == stday:  jday = date(2005,12,20)
            elif jday < stday: jday = dateAddmonth(jday,12)     # 调整前按年结息
            else:              jday = dateAddmonth(jday,3)      # 调整后按季结息
        if pre < oday:  # 算头不算尾
            sum_ += round(self.cur_tax_later_sep(pre,oday,sum_,_daysFunc(oday,real),sep),3)
        return round(sum_,2) if sep is None else (round(sum_,2),sep)
        
    def lumpCalc_sep(self,record,oday,sep,real): # 整存整取本息和
        pre = record.day
        mon = record.month
        next = dateAddmonth(pre,mon)
        sum_ = round(record.money,2)
        sep = [] if sep else None
        
        def _step():
            nonlocal sum_,sep,pre,next,self,mon
            i = self.table.icor(pre)
            r = 0.0 if i<0 else self.table.getTheIrate(i,mon)
            sqx = _preint(r,sum_,mon)
            s = self.tax(pre,next,sum_,r)
            if sep is not None:
                sep.append([pre,next,sum_,r,sqx,s])      
            sum_ += round(sqx-s,3)
            pre = next
            
        if oday >= next:
            _step()
            if record.again != 0:   # 有转存
                n = mon + record.again
                mon = record.again
                next = dateAddmonth(record.day,n)
                while oday >= next:
                    _step()
                    n += mon
                    next = dateAddmonth(record.day,n)
        if pre < oday:  
            # 若不转存且 pre < oday 也不算活期计息日结息,毕竟是定期储蓄
            sum_ += round(self.cur_tax_later_sep(pre,oday,sum_,_daysFunc(oday,real,pre!=record.day),sep),3)
        return round(sum_,2) if sep is None else (round(sum_,2),sep)
            
    def cur_lump_calc_sep(self,record,oday,sep,real=False):# 活期或整存整取本息和
        return (self.curCalc_sep if record.month is 0 else self.lumpCalc_sep)(record, oday, sep, real)
        
def lsUpdate(calc,lsfiles=_RATES_FILE):      # 联网更新利率 
    lsupdate.ghlsUpdate2(lsfiles)    
    # lsupdate.crawler_async2().ghlsUpdate_async(lsfiles)
    # 当禁止程序联网后无响应(异步)
    calc.table.tableInit(lsfiles)
    if calc.table.empty():
        raise ValueError('建表失败(费率文件中数据有错)，费率表被清空...')

import pickle as pk 
import zlib
DEFAULT_IRLIB = _get_path('calc.cib')
        
class irecordLib:  # 存单库
    def __init__(self,libfile=DEFAULT_IRLIB):
        self.autoLoad(libfile)
    def empty(self):
        return not self.irs
        
    def autoLoad(self,file):
        self.irs = []
        with open(file,'ab+') as f:
            f.seek(0,0)
            try:
                tmp = zlib.decompress(f.read())
                self.irs = pk.loads(tmp) 
            except:
                self.irs = []
                
    def save(self,path):
        with open(path,'wb') as f:
            bir = pk.dumps(self.irs)
            f.write(zlib.compress(bir,9))
                
    def append(self,irecord):
        no = self.irs[-1].num+1 if self.irs else 1
        irecord.num = no
        self.irs.append(irecord)
    
    def show(self):
        if self.irs:
            return '\n'.join(map(str,self.irs))
        return 'Empty...'
    def find(self,no):
        for ir in self.irs:
            if ir.num == no:return ir
    def clear(self):
        self.irs.clear()
        
#---------------------test()---------------------
if __name__ == '__main__':
    import logging
    
    class FilterSep(logging.Filter): # for test()
        def __init__(self,no):
            super().__init__()
            self.levelno = no
            
        def filter(self,record):
            return (record.levelno <= self.levelno)
            
    class logger:   # for test()
        m_logger = None
        def __init__(self):
            if not self.__class__.m_logger:
                self._Init()
        def _Init(self): 
            log = logging.getLogger()
            log.setLevel(20)
            
            console = logging.StreamHandler()
            console.addFilter(FilterSep(60))
            # console.setLevel(30)
            
            fhand = logging.FileHandler(_get_path('calc.log'))
            fhand.setLevel(60)
            
            log.addHandler(console)
            log.addHandler(fhand)
            
            self.__class__.m_logger = log
            
        @classmethod
        def getLogger(cls):
            if not  cls.m_logger:
                logger()
            return cls.m_logger
            
    def test(): 
        def _type(type):
            return '对账版' if type else '普通版'
            
        x=lumpCalc(ratesTable(_RATES_FILE))
        if x.table.empty():
            print('无费率文件或费率文件有错...')
            input()
            return 
            
        clog = logger.getLogger()    
        pat = re.compile(r'\d+\.?\d*')
        type = False
        menu = ['\n整存整取(活期)本息和计算器【{}】',
                '退出程序：q | Q',
                '显示费率: s | S',
                '更新利率: update',
                '版本选择：dz',
                '活期存期计算方式：0(360/30) 1(实际天数)',
                '或直接输入：存日 本金 存期 转存期(空格间隔)']
        menu = '\n\t'.join(menu)
        real = 0
        while True :
            print(menu.format(_type(type))) 
            print('>> ',end='')
            s = input().strip()
            # s=s.strip().lower()
            # if s=='q':
            if re.match(r'^[q|Q]\s*$',s) :
                break
            # elif s=='s':
            elif re.match(r'^[s|S]\s*$',s) :
                print(x.table.strTable())
            # elif s=='update':   
            elif re.match(r'^update\s*$',s):
                try:
                    lsUpdate(x,_RATES_FILE)
                except Exception as er:
                    print('ERROR:{}'.format(er))
            # elif s=='dz':
            elif re.match(r'^dz\s*$',s):
                type = not type   # 直接取反
            elif s=='0' or s=='1':
                real = int(s)
            else :
                s = '1998 1 1 1000 3 3'
                ir = strToird(s)
                if  ir is not None:
                    try:
                        if not ir.checkMonth() :
                            print('ERROR: 存期/转存期有误...')
                            continue                        
                        s = input('请输入取出日期(回车按当前日期):\n>> ')
                        s = '2015 12 17'
                        r = pat.findall(s)
                        if len(r) == 0 :
                            d = date.today()
                        else:                
                            d = date(int(r[0]),int(r[1]),int(r[2]))
                    except ValueError :
                        print('ERROR: 日期输入错误...')  
                    else :                        
                        res = x.cur_lump_calc_sep(ir,d,type,real)
                        ANY = 60 if type else 50                        
                        clog.log(ANY,'%s\n%s\n       取出日期: %s','-'*60,ir,d)
                        clog.log(ANY,'本息和(税后):%.2f',res[0] if type else res)
                        if type and res[1]:
                            st = ['{}  {}  {:^14.3f}  {:^5.2f}  {:^13.3f}  {:<.3f}'.format(*i) for i in res[1]]
                            clog.log(70,'        时间段        \t    本金    \t利率\t  税前息  \t    税')
                            clog.log(70,'\n'.join(st))
                else :
                    print('ERROR:请按格式输入...')
            print('-'*60)
    # ------------------------------------------      
    import profile as ce 
    _RATES_FILE = _get_path('ls.csv')
    ce.run('test()')
    # toCsvFile(_get_path('ls.csv'))
